feat: extract task comment timeline into the Collaboration microservice#84
Merged
Merged
Conversation
…ment timeline
Extract the task comment timeline ('ветки') — user, genesis and system
comments — into a dedicated Collaboration microservice, fully consistent
with the existing service template (clean architecture, BuildingBlocks,
gRPC+ServiceKey, Outbox, Serilog/OTEL, health checks, Docker, Ocelot).
- New Planora.Collaboration.{Domain,Application,Infrastructure,Api} projects
- Comment aggregate + repository + EF config (schema 'collaboration')
- CQRS handlers: add/genesis/update/delete/get comments
- Inbox consumers materialise system/genesis comments and cascade-delete
from Todo lifecycle integration events; clean up on user deletion
- Comment notifications published to Outbox -> NotificationEvent -> Realtime
- Task access authorised via Todo gRPC (CheckTaskCommentAccess) so no
cross-service DB reads (INV-OWN-1); avatars via Auth gRPC (cached)
- Shared integration-event contracts (TaskCreated/TaskActivity/TaskDeleted)
- Wiring: solution, docker-compose, Ocelot routes, Migrator, Todo gRPC port
- Frontend comment API calls repointed to /collaboration/api/v1/comments
Todo-side removal of comment code follows in a subsequent commit.
…cycle events TodoApi no longer owns any 'ветки' code. The comment entity, repository, DTO, 5 CQRS handlers, avatar gRPC client and REST endpoints are deleted. Task lifecycle now drives the Collaboration timeline via integration events published through a new Todo outbox (INV-COMM-3): - CreateTodo -> TaskCreatedIntegrationEvent (system 'created' + genesis) - Update/Join/Leave -> TaskActivityIntegrationEvent (completed/started/left) - DeleteTodo -> TaskDeletedIntegrationEvent (cascade soft-delete) - TodoGrpcService.CheckTaskCommentAccess exposes the task access decision (owner/shared/public + friendship) + participants for Collaboration - TodoDbContext gains OutboxMessages; OutboxProcessor registered; Todo gRPC endpoint published on :81 in Docker - EF migration drops todo.todo_item_comments and creates todo.OutboxMessages - Tests: comment-only suites removed, handler fixtures switched to the outbox ctor, Comment domain tests ported to Collaboration, arch tests cover it
…variants - Planora.Migrator --backfill-collaboration: idempotent copy of todo.todo_item_comments -> collaboration.comments (run before route cutover) - docs/INVARIANTS.md: INV-OWN-1 adds Collaboration DB; INV-AZ-4 documents that comment-thread friendship authorisation is delegated to TodoApi via the CheckTaskCommentAccess gRPC contract
…trap - TaskAccessGrpcClient wraps gRPC faults in ExternalServiceUnavailableException (DomainException -> HTTP 503 via shared middleware), matching TodoApi semantics - Honour OperationCanceledException without remapping - deploy/fly/collaboration-service.fly.toml added (mirrors todo-service) - deploy/fly/postgres-init.sql creates planora_collaboration
…t matrix - ExternalServiceUnavailableException now mirrors TodoApi exactly (DomainException with ErrorCode.Infrastructure.ExternalServiceUnavailable + ServiceUnavailable category + inner exception) — the previous ctor signature did not exist - Rename deploy/fly manifest to collaboration.fly.toml matching the repo naming and structure (internal gRPC over :443, /health/live + /health/ready checks) - set-secrets.ps1: planora-collaboration joins the secret matrix (shared + DB + Auth/Todo gRPC addresses); migrator gains CollaborationDatabase
Duplicate 'using ...Application.Context;' would fail the -warnaserror build (CS0105). Verified no duplicate usings remain across Todo/Collaboration.
…on-comments-microservice # Conflicts: # Services/CollaborationApi/Planora.Collaboration.Infrastructure/Persistence/Configurations/CommentConfiguration.cs # docker-compose.yml # tests/Planora.UnitTests/Architecture/ArchitectureTests.cs # tools/Planora.Migrator/Planora.Migrator.csproj # tools/Planora.Migrator/Program.cs
…ypass) The repo .gitignore lists **/Migrations/**; existing Todo migrations were force-added historically. 'git add -A' silently skipped the new migration, so the DROP of todo_item_comments + creation of todo.OutboxMessages was absent from history while the model snapshot already reflected them — a snapshot/DB drift that would make MigrateAsync fail or leave the schema wrong. Force-add both the migration and its designer so the schema change ships.
…/API/architecture docs Tests (Collaboration): - CommentCommandHandlerTests: access matrix for add/genesis/update/delete (grant/deny/not-found, owner-only genesis, dup-genesis guard, author-vs-owner delete rules, notification fan-out count) - IntegrationEventConsumerTests: TaskCreated (system+genesis, replay-safe), TaskActivity (completed/started/left + unknown-type skip), TaskDeleted cascade, UserDeleted authored-comment cleanup + no-op Docs: - database.md: Collaboration DB section, ownership row, Todo OutboxMessages, RemoveCommentsAddOutbox migration, corrected gitignore/force-add note, backfill - API.md: Collaboration endpoint section + gateway routes; Todo comment routes removed - architecture.md: service list, boundaries, gRPC access delegation, event flow
…le coverage Docs (all service-facing pages now reflect the Collaboration extraction): - codebase-map.md: Collaboration Service section; Todo critical-files updated - features.md: Task Comments rewritten around the Collaboration service + event flow - security-idor-coverage.md: comment rows repointed to /collaboration + new coverage, worker rows repointed off the deleted test file - testing.md: Collaboration test inventory; worker lifecycle now event-based - overview.md / index.md / glossary.md: Collaboration listed in services, ownership, terms Tests: - WorkerLifecycleEventTests.cs: pins that Join/Leave publish the correct TaskActivityIntegrationEvent via outbox (restores coverage lost when the comment-coupled WorkersAndComments test suite was removed), owner-cannot-leave guard
- CollaborationApi/Program.cs: add missing using for Planora.BuildingBlocks.Infrastructure.Configuration so AddPlanoraSwaggerGen / UsePlanoraSwagger resolve (CI 'backend' -warnaserror build break, CS1061) - CHANGELOG.md: use '*' list markers in the new section to match the file's established bullet style (CI 'docs' MD004/ul-style consistency)
…Tests TodoItemDto has 10 required init members; the mapper mock only set 4, failing the -warnaserror backend build (CS9035). Populate every required member.
Badges, architecture diagram + service/data-store table, tech-stack matrix, engineering-principles (invariants) section, Docker-first quickstart, config table, testing/CI summary, project structure, and a documentation index. Adds the Collaboration service that the previous README omitted.
…xUnit2013) xUnit2013 (do not use Assert.Equal for collection size) is warning-as-error in this repo (-warnaserror); the codebase uses zero such patterns. The new TaskCreated consumer test tripped it, failing the test-project build. Switch to Assert.Collection, which also pins the [system, genesis] ordering.
4Keyy
pushed a commit
that referenced
this pull request
May 30, 2026
The merged PR #84 left the backend build red on develop: WorkerLifecycleEventTests referenced IUnitOfWork without importing Planora.BuildingBlocks.Domain.Interfaces (CS0246). Add the using so 'dotnet build -warnaserror' passes.
4Keyy
added a commit
that referenced
this pull request
May 31, 2026
The #84 Collaboration microservice extraction left CI and the security scan red on develop. Four independent breakages are fixed here so every gate is green again. WHAT changed and WHY: - TodoGrpcServiceTests: the gRPC service constructor gained ITodoRepository and IFriendshipService (for CheckTaskCommentAccess), but the test factory still passed the old two-arg shape (CS7036). Updated CreateService to supply default mocks for both dependencies. - WorkerLifecycleEventTests: replaced Assert.True(.Any(...)) with Assert.Contains to satisfy the xUnit2012 analyzer promoted to error under -warnaserror. - docs/database.md: a wrapped bullet line began with '+ shared-with', which markdownlint parsed as a plus-style list marker (MD004). Rewrapped so the continuation no longer starts with a list glyph. - Collaboration Dockerfile: added --no-install-recommends to the apt-get install, matching every other service Dockerfile and clearing the Trivy IaC HIGH that blocked the security scan. HOW verified: full backend build with -warnaserror, dotnet test (788 passed), markdownlint with the CI globs (0 errors), and the frontend lint/type-check/test/build pipeline all pass locally. The CodeQL csharp job failed only because autobuild hit the same CS7036 compile error, so it recovers with the test fix. Security: restores the Trivy IaC HIGH/CRITICAL gate to green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4Keyy
added a commit
that referenced
this pull request
May 31, 2026
…cher The #84 Collaboration microservice extraction added a new service that the local launcher never started, and the launcher carried four hand-maintained port arrays that had already drifted from reality. This brings the launcher in line with the current topology and makes its port handling impossible to desync. WHAT changed and WHY: - Start collaboration-api: added it to $ServiceDefs at port 5060, placed after auth-api and todo-api because it is a gRPC client of both (Auth 5031, Todo 5101). It now builds, starts, health-checks, appears in the summary table, and is torn down with the rest. - Single source of truth for ports: $ServiceRestPorts, $ServiceGrpcPorts, $AllPlanoraPorts, and $ShutdownOrder are now derived from $ServiceDefs. Stop-PlanoraProcesses and Invoke-GracefulShutdown use them, replacing four divergent literal arrays (which omitted 5060 and disagreed on gRPC ports). - New -Stop switch: stops every process the launcher started and frees their ports without touching infrastructure containers or data volumes, reusing the existing graceful-shutdown path. - New -Help switch: prints a concise usage block and exits before any side effect (no transcript/log file created). HOW verified: the script parses cleanly via the PowerShell AST parser, and both -Help and -Stop were run end-to-end (Stop exercises the derived port lists and shutdown order and exits 0). README and getting-started.md document the new flags; CHANGELOG records the change. Security: -Stop deliberately skips loading .env, so no secrets enter the process environment for a teardown-only invocation
4Keyy
added a commit
that referenced
this pull request
May 31, 2026
UpdateCommentCommandValidator capped every comment edit at 2000 characters, but a genesis comment (the task description) is allowed up to 5000 - both on create and in the domain (Comment.UpdateGenesisContent). Because the validator runs in the ValidationBehavior pipeline before the handler, editing a task description to 2001-5000 characters was rejected with a 400, contradicting the domain, docs/features.md, and the documented PUT behavior in docs/API.md. The validator only receives the ids and content - it cannot tell a genesis comment from a regular one - so it now enforces only the shared upper bound (5000). The domain applies the exact per-kind limit (2000 regular via Comment.UpdateContent, 5000 genesis via UpdateGenesisContent) and a violation surfaces as a 400 (InvalidValueObjectException is ErrorCategory.Validation). Added UpdateCommentCommandValidatorTests pinning the boundaries (accepts 3000 and 5000, rejects empty and 5001, requires TaskId and CommentId). The full Collaboration suite passes (40 tests). docs/API.md and docs/features.md already described the intended 5000 limit, so the code was the defect, not the docs. Refs: #84
4Keyy
added a commit
that referenced
this pull request
May 31, 2026
… live author identity Removes a cross-service data-duplication bug class. The task description was stored twice — TodoItem.Description (the card) and a 'genesis' comment in the Collaboration DB (the branch) — synced only at creation via an async event. So pre-Collaboration tasks had an empty branch, new tasks' descriptions lagged the outbox cycle, and the two edit paths could diverge. P1 — description = single source of truth (Todo): - CheckTaskCommentAccess gRPC now returns the live description + task_created_at. - GetCommentsQueryHandler synthesises the pinned 'Author's Note' from it on read (page 1 only; id = task id, author = owner) instead of reading a stored genesis. Instant, always matches the card, present for old tasks. - TaskCreatedEventConsumer no longer materialises a genesis (only the 'created the task' system comment). Removed the POST /genesis endpoint, the AddGenesisComment command/handler/validator, and Comment.CreateGenesis / UpdateGenesisContent. The read query excludes any legacy genesis rows. - Frontend edits the description on the task (PUT /todos) via a new onSaveDescription path in the edit modal; branch-feed no longer calls genesis endpoints. Removed the dead addGenesisComment API client function. P2 — author identity resolved live: - Comment.AuthorName was a stored copy of the Auth-owned name that went stale on rename. Added AuthService.GetUserProfilesBatch (name + avatar); Collaboration resolves comment + genesis author identity live (60 s cache via CachingUserService), keeping the stored name only as an offline fallback. IUserService is now profile-based (GetUserProfilesAsync) instead of avatar-only. HOW verified: end-to-end on a live local stack — a task created with a description returns the Author's Note on an immediate (0s) fetch with the author name resolved live, and editing the description on the task reflects in the branch. Backend builds under -warnaserror; 784 backend + 370 frontend tests pass on net10.0. Docs (API, features, database, architecture) updated to the new model. Refs: #84
4Keyy
added a commit
that referenced
this pull request
Jun 1, 2026
Audit follow-ups (P1 + P2). P1 - consumer idempotency (INV-COMM-4) was documented but not implemented. RabbitMqEventBus delivers at-least-once (nack+requeue on failure), but nothing deduped - the IdempotentMessageHandler/Inbox machinery was dead code, so a redelivered or restart-replayed event produced duplicate system comments (Collaboration) / notifications. The bus now dedups centrally on the stable @event.Id via IInboxRepository: skip the handler when the id is already recorded, record it after success. Graceful + defensive - a service with no inbox (or an inbox error) falls back to the previous behaviour, never worse. Added an InboxMessages table + repository to Collaboration (PK = event id); Realtime is a follow-up. Also fixed the pre-existing bug where IdempotentMessageHandler checked ExistsAsync against the PK while storing the id in MessageId (never matched) - the new InboxMessage(Guid eventId,...) ctor makes the PK the event id. P2 - AsNoTracking on CategoryApi reads. BaseRepository already does this for its generic reads, but the custom CategoryRepository did not. Added it to the read-only methods (list/get/paged); kept tracking on GetByIdAsync (load-then-update) and FindAsync (also used for fetch-then-RemoveRange). HOW verified: build clean under -warnaserror; 784 tests pass on net10.0; live stack confirmed the inbox records the processed TaskCreatedIntegrationEvent and exactly one 'created the task' system comment is produced. Docs (database.md, features.md) updated; the Collaboration inbox table is created by EnsureCreated on a fresh DB. Refs: #84
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Extracts the task comment timeline ("ветки") — user, genesis, and system comments — out of TodoApi into a dedicated Collaboration microservice. This gives the timeline its own bounded context, database, and lifecycle, so comments can evolve (and scale) independently of the task aggregate while keeping TodoApi focused purely on tasks.
The new service follows the platform template exactly: clean architecture, BuildingBlocks wiring, Serilog + OpenTelemetry, shared global exception middleware, JWT + security-stamp validation, rate limiting, response compression, health endpoints, the Outbox pattern, and a non-root Dockerfile.
Type of change
Responsibility split
TaskCreatedIntegrationEvent,TaskActivityIntegrationEvent(completed/started/left),TaskDeletedIntegrationEvent— and exposesTodoService.CheckTaskCommentAccessover gRPC. The EF migrationRemoveCommentsAddOutboxdropstodo_item_commentsand addstodo.OutboxMessages.collaboration.comments(databaseplanora_collaboration, gateway prefix/collaboration/api/v1/comments). It authorises every read/write via the Todo gRPC access check (owner / shared / public + friendship — never reading Todo's DB, INV-OWN-1), materialises system/genesis comments from the Todo events through idempotent Inbox consumers (INV-COMM-4), and fans out aNotificationEventper participant on each new comment (Outbox → RabbitMQ → Realtime/SignalR).Errors, validation & security
DomainException(ExternalServiceUnavailableException); the shared middleware also mapsRpcExceptionnatively.ValidationBehavior.[Authorize]on the controller; every operation re-checks task access server-side. gRPC clients carry thex-service-key(INV-COMM-2); JWT + security-stamp revocation wired (INV-AUTH-4).Data migration
Planora.Migrator --backfill-collaborationidempotently copiestodo.todo_item_comments→collaboration.comments(INSERT ... ON CONFLICT (Id) DO NOTHING). Run it before applyingRemoveCommentsAddOutboxin production, and again at cutover to capture the window.Frontend
Comment API calls repoint to
/collaboration/api/v1/comments/*; theCommentDtoJSON shape is unchanged, so the timeline UI is untouched. Existing develop frontend (CSRF retry, trace-id propagation, request cancellation) preserved through the merge.Testing
dotnet test Planora.sln) — Collaboration domain, handler (access matrix + notification fan-out), and integration-event consumer (replay-safe materialisation, cascade, user-deletion) suites, plusWorkerLifecycleEventTestspinning the new event-based worker lifecycle in TodoApi.Checklist
.env.exampleupdated if new env vars were added (reuses existing shared vars; addedGrpcServices__TodoApiusage + Collaboration entry indeploy/fly/set-secrets.ps1)architecture,database,API,codebase-map,features,testing,security-idor-coverage,overview,index,glossary,INVARIANTS)## Unreleasedhttps://claude.ai/code/session_019ETjrb3oYgitUtT6ABM7hF
Generated by Claude Code